# class Store: read, set, commit
# class Record: read, set (tracks own store)
# 

class Store(object):
	"The Store class represents a file with DNS data in tinydns format."
	def __init__(self, datafile=None, zone=None):
		"Set the zone's name and parse its records from the source file"
		self.records = {}
		self.zone = zone
		if datafile is None:
			self.source_name = '/etc/tinydns/data'
		else:
			self.source_name = datafile
		try:
			self.source = file(self.source_name, 'r')
			self.read()
		except IOError:
			self.source = None
		
		
	def read(self):
		"Parse each record in the source file"
		for line in self.source:
			record = Record.parse(line)
			if record:
				self.add(record)
			
	def write(self):
		"Write the records for each FQDN in the store"
		if self.source:
			self.source.close()
		w = file(self.source_name, 'w')
		for fqdn in self.records:
			for r in self.records[fqdn]:
				w.write(r.write() + '\n')
	
	def save(self):
		"Save the file, then trigger remote updates"
		self.write()
		try:
			from django.conf import settings
			command = settings.UPDATE_COMMAND
		except AttributeError:
			command = None
		if command:
			import os
			os.system(command)
	
	def add(self, record):
		# Make sure that the FQDN belongs to this store's zone
		if self.zone:
			assert record.fqdn[-len(self.zone):] == self.zone
		# Add a list for this record, or append it to the existing list
		if record.fqdn not in self.records:
			self.records[record.fqdn] = [record]
		else:
			self.records[record.fqdn].append(record)
			
	def retrieve(self, fqdn, rtype):
		try:
			fqdn_records = self.records[fqdn]
		except KeyError:
			return []
		record_list = []
		for record in fqdn_records:
			if record.isinstance(rtype):
				record_list.append(record)
		return record_list
	
	def isinstance(self, rtype):	
		if not isinstance(rtype, Record):
			rtype = {'NS': NSRecord, 'NNS': NNSRecord,
				'APTR': APTRRecord, 'A': ARecord}[rtype]
		return isinstance(self, rtype)
			
	def delete(self, fqdn, rtype):
		for record in self.records[fqdn]:
			if record.isinstance(rtype):
				self.records[fqdn].remove(record)
	
	def update(self, fqdn, rtype, **kwargs):
		# FIXME: this code will only update the last record
		# that has a given rtype
		record = None
		try:
			for existing_record in self.records[fqdn]:
				if isinstance(existing_record, rtype):
					record = existing_record
		except KeyError:
			self.records[fqdn] = []
		if not record:
			record = rtype()
			self.records[fqdn].append(record)
		record.fqdn = fqdn
		for kw in kwargs:
			setattr(record, kw, kwargs[kw])
			
class Record(object):
	TYPES = ('NS', 'NNS', 'APTR', 'A', 'MX', 'COMMENT', 'IGN',
	 	'TXT', 'PTR', 'CNAME', 'SOA', 'G')
	class InvalidStartCharacter(Exception):
		pass
	class WrongFieldCount(Exception):
		pass
	def __init__(self, source, line):
		self.source = source
		self.line = line

	@classmethod
	def parse(cls, line):
		if line == None:
			line = cls.line
		line = line.lstrip()
		if len(line) == 0:
			return None
		t = line[0]
		l = line[1:].split(':')
		if t == '.':
			return NSRecord.parse(line)
		elif t == '&':
			return NNSRecord.parse(line)
		elif t == '=':
			return APTRRecord.parse(line)
		elif t == '+':
			return ARecord.parse(line)
		elif t == '@':
			return MXRecord.parse(line)
		elif t == '#':
			return CommentRecord.parse(line)
		elif t == "'":
			return TXTRecord.parse(line)
		elif t == "^":
			return PTRRecord.parse(line)
		elif t == "C":
			return CNAMERecord.parse(line)
		elif t == 'Z':
			return SOARecord.parse(line)
		elif t == ':':
			return GenericRecord.parse(line)
		else:
			raise cls.InvalidStartCharacter(t)
	def set_ttl_ts_lo(self, *args):
		try:
			self.ttl = args[0]
			self.timestamp = args[1]
			self.location = args[2]
		except IndexError:
			pass
			
class TypedRecord(Record):
	optionalfields = []
	
	def __init__(self, **kwargs):
		for kw in kwargs:
			setattr(self, kw, kwargs[kw])
			
	@classmethod
	def parse(cls, line):
		# Split fields
		data = line[1:].split(':')
		# Create an instance
		r = object.__new__(cls)
		# Set the first character
		r.start = line[0]
		# Initialize the field counter
		fn = 0
		# Match each field value (in the data array) to its name
		# (in the fields and optionalfields class attributes)
		for f in cls.fields:
			setattr(r, f, data[fn])
			fn = fn + 1
		try:
			try:
				for f in cls.optionalfields:
					setattr(r, f, data[fn])
					fn = fn + 1
			except AttributeError:
				pass
		except IndexError:
			return r
		# Set the last three values
		r.set_ttl_ts_lo(*data[fn:fn+3])
		# The record is unchanged as yet
		r.modified = False
		return r
		
	#def __getattr__(self, attr, *args, **kwargs):
	#	"""If we are looking for optional fields and there are none,"""
	#	try:
	#		return super(TypedRecord, self).__getattr__(attr, *args, **kwargs)
	#	except AttributeError:
	#		if attr == 'optionalfields':
	#			return []
	#		else:
	#			raise
		
	def __setattr__(self, attr, val):
		"""
		Make sure that the attribute we are setting is valid for this
		kind of record
		"""
		try:
			getattr(self, 'modified')
			super(TypedRecord, self).__setattr__('modified', True)
		except AttributeError:
			pass
		if isinstance(val, int):
			val = str(val)
		assert (attr in self.fields or attr in self.optionalfields
			or attr in ('ttl', 'timestamp', 'lo', 'start', 'modified'))
		super(TypedRecord, self).__setattr__(attr, val)
	
	def write(self):
		class MissingField(Exception):
			pass
		for f in self.fields:
			if getattr(self, f, MissingField) == MissingField:
				raise MissingField('Missing %s in %s' % (f, self))
		f = list(self.fields)
		try:
			f = f + list(self.optionalfields)
		except AttributeError:
			pass
		f = f + ['ttl', 'timestamp', 'lo']
		params = [getattr(self, fieldname, '')
			for fieldname in f]
		return self.start + ':'.join(params).rstrip(':')	
		
class UnparsedRecord(TypedRecord):
	@classmethod
	def parse(cls, data):
		r = object.__new__(cls)
		r.data = data
		return r
			
class NSRecord(TypedRecord):
	fields = ('fqdn', 'ip', 'name')
	
class NNSRecord(NSRecord):
	pass

class APTRRecord(TypedRecord):
	fields = ('fqdn', 'ip')
	start = '='
	
class ARecord(TypedRecord):
	fields = ('fqdn', 'ip')
	
class MXRecord(TypedRecord):
	fields = ('fqdn', 'ip', 'mxname', 'dist')
	
class CommentRecord(UnparsedRecord):
	pass
	
class TXTRecord(TypedRecord):
	fields = ('fqdn', 'string')
	
class PTRRecord(TypedRecord):
	fields = ('fqdn', 'domainname')
	
class CNAMERecord(TypedRecord):
	fields = ('fqdn', 'domainname')
	
class SOARecord(TypedRecord):
	fields = ('fqdn', 'ns', 'contact')
	optionalfields = ('ser', 'ref', 'ret', 'exp', 'min')
	
class GenericRecord(TypedRecord):
	fields = ('fqdn', 'type', 'rdata')

